package com.thz.system.service.pdf.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.thz.common.dto.pdf.ConversionDocxToPdfRequestDTO;
import com.thz.common.dto.pdf.ConversionDocxToPdfResponseDTO;
import com.thz.common.dto.user.UserInfoDTO;
import com.thz.common.entity.ConversionRecord;
import com.thz.common.entity.ConversionRecordDocxToPdf;
import com.thz.common.enums.ConversionStatus;
import com.thz.common.enums.ConversionType;
import com.thz.common.enums.ErrorCode;
import com.thz.common.exception.BusinessException;
import com.thz.common.utils.FileUtils;
import com.thz.system.config.CosConfig;
import com.thz.system.config.DocumentConverterProperties;
import com.thz.system.mapper.pdf.ConversionDocxToPdfMapper;
import com.thz.system.service.pdf.IConversionDocxToPdfService;
import com.thz.system.service.record.IConversionRecordService;
import com.thz.system.service.user.IUserInfoService;
import com.thz.system.utils.cos.CosFileUtils;
import com.thz.system.utils.pdf.LibreOfficeConverter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * DOCX 转 PDF 服务实现
 */
@Slf4j
@Service
public class ConversionDocxToPdfServiceImpl implements IConversionDocxToPdfService {

    @Autowired
    private ConversionDocxToPdfMapper conversionDocxToPdfMapper;

    @Autowired
    private IUserInfoService userInfoService;

    @Autowired
    private IConversionRecordService conversionRecordService;

    @Autowired
    private CosFileUtils cosFileUtils;

    @Autowired
    private CosConfig cosConfig;

    @Autowired
    private LibreOfficeConverter libreOfficeConverter;

    @Autowired
    private DocumentConverterProperties documentConverterProperties;

    @Value("${app.domain:http://localhost:9091}")
    private String serverDomain;

    private Semaphore semaphore;

    @PostConstruct
    public void initSemaphore() {
        int permits = Math.max(1, documentConverterProperties.getMaxConcurrency());
        semaphore = new Semaphore(permits, true);
        log.info("DOCX转PDF并发锁已初始化，最大并发: {}", permits);
    }

    @Override
    public ConversionDocxToPdfResponseDTO convertDocxToPdf(MultipartFile file,
                                                           ConversionDocxToPdfRequestDTO requestDTO,
                                                           String ipAddress) {
        FileUtils.validateDocumentFile(file);
        if (requestDTO == null) {
            requestDTO = new ConversionDocxToPdfRequestDTO();
        }

        UserInfoDTO userInfoDTO = userInfoService.getOrCreateUser(ipAddress);

        ConversionRecordDocxToPdf record = new ConversionRecordDocxToPdf();
        record.setOriginalFilename(file.getOriginalFilename());
        record.setOriginalFileSize(file.getSize());
        record.setOriginalFileType(file.getContentType());
        record.setConversionStatus(ConversionStatus.PENDING.getCode());
        record.setIpAddress(ipAddress);
        record.setUserId(userInfoDTO.getId());
        record.setCreateTime(LocalDateTime.now());
        record.setUpdateTime(LocalDateTime.now());
        conversionDocxToPdfMapper.insert(record);

        boolean acquired = false;
        File workingDir = null;
        File inputFile = null;
        File pdfFile = null;
        File uploadFile = null;
        try {
            acquired = semaphore.tryAcquire(1, 2, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BusinessException(ErrorCode.SERVER_BUSY);
            }

            record.setConversionStatus(ConversionStatus.CONVERTING.getCode());
            conversionDocxToPdfMapper.updateById(record);

            workingDir = createWorkingDirectory();
            inputFile = saveMultipartFile(file, workingDir);
            record.setOriginalFilePath(inputFile.getAbsolutePath());
            conversionDocxToPdfMapper.updateById(record);

            String convertParam = buildConvertToParam(requestDTO);
            pdfFile = libreOfficeConverter.convertToPdf(inputFile, convertParam);
            uploadFile = renameForUpload(pdfFile);

            String pdfPath = cosFileUtils.uploadLocalFile(uploadFile, buildPdfOutputPrefix());

            record.setPdfFilename(uploadFile.getName());
            record.setPdfFilePath(pdfPath);
            record.setPdfFileSize(uploadFile.length());
            record.setConversionStatus(ConversionStatus.SUCCESS.getCode());
            record.setErrorMessage(null);
            record.setUpdateTime(LocalDateTime.now());
            conversionDocxToPdfMapper.updateById(record);

            createUnifiedRecord(record);
            userInfoService.incrementConversions(ipAddress);

            return buildResponseDTO(record);
        } catch (BusinessException e) {
            handleFailure(record, e.getMessage());
            throw e;
        } catch (Exception e) {
            log.error("DOCX转PDF失败", e);
            handleFailure(record, e.getMessage());
            throw new BusinessException(ErrorCode.CONVERSION_FAILED, e.getMessage(), e);
        } finally {
            if (acquired) {
                semaphore.release();
            }
            cleanupFile(inputFile);
            cleanupFile(pdfFile);
            cleanupFile(uploadFile);
            cleanupDirectory(workingDir);
        }
    }

    @Override
    public ConversionDocxToPdfResponseDTO getConversionRecord(Long id) {
        ConversionRecordDocxToPdf record = conversionDocxToPdfMapper.selectById(id);
        if (record == null) {
            throw new BusinessException(ErrorCode.CONVERSION_RECORD_NOT_EXISTS);
        }
        return buildResponseDTO(record);
    }

    @Override
    public List<ConversionDocxToPdfResponseDTO> getRecentConversions(int limit) {
        LambdaQueryWrapper<ConversionRecordDocxToPdf> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(ConversionRecordDocxToPdf::getCreateTime).last("LIMIT " + limit);
        return conversionDocxToPdfMapper.selectList(wrapper)
                .stream()
                .map(this::buildResponseDTO)
                .collect(Collectors.toList());
    }

    @Override
    public void deleteConversion(Long id) {
        ConversionRecordDocxToPdf record = conversionDocxToPdfMapper.selectById(id);
        if (record == null) {
            throw new BusinessException(ErrorCode.CONVERSION_RECORD_NOT_EXISTS);
        }
        cosFileUtils.deleteFile(record.getPdfFilePath());
        conversionDocxToPdfMapper.deleteById(id);
    }

    private void handleFailure(ConversionRecordDocxToPdf record, String message) {
        record.setConversionStatus(ConversionStatus.FAILED.getCode());
        record.setErrorMessage(message);
        record.setUpdateTime(LocalDateTime.now());
        conversionDocxToPdfMapper.updateById(record);
    }

    private File createWorkingDirectory() throws IOException {
        Path base = documentConverterProperties.resolveTempDir().toPath();
        Path dir = Files.createTempDirectory(base, "docx2pdf-");
        return dir.toFile();
    }

    private File saveMultipartFile(MultipartFile multipartFile, File workingDir) throws IOException {
        String extension = FileUtils.getFileExtension(multipartFile.getOriginalFilename());
        String baseName = FileUtils.sanitizeFilename(FileUtils.getFileBaseName(multipartFile.getOriginalFilename()));
        if (baseName.isBlank()) {
            baseName = "document";
        }
        String filename = baseName + "_" + System.currentTimeMillis() + "." + extension;
        File target = new File(workingDir, filename);
        multipartFile.transferTo(target);
        return target;
    }

    private File renameForUpload(File pdfFile) throws IOException {
        if (pdfFile == null) {
            return null;
        }
        String newName = FileUtils.generateUniqueFilename(pdfFile.getName());
        File target = new File(pdfFile.getParentFile(), newName);
        Files.move(pdfFile.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
        return target;
    }

    private String buildPdfOutputPrefix() {
        String prefix = cosConfig.getOutputPrefix();
        if (prefix == null) {
            prefix = "output/";
        }
        if (!prefix.endsWith("/")) {
            prefix += "/";
        }
        return prefix + "docx/";
    }

    private String buildConvertToParam(ConversionDocxToPdfRequestDTO dto) {
        List<String> options = new ArrayList<>();
        if (Boolean.TRUE.equals(dto.getPdfa())) {
            options.add("SelectPdfVersion=1");
        }
        if (Boolean.TRUE.equals(dto.getEmbedStandardFonts())) {
            options.add("EmbedStandardFonts=true");
        }
        if (Boolean.FALSE.equals(dto.getExportBookmarks())) {
            options.add("ExportBookmarks=false");
        }
        if (options.isEmpty()) {
            return "pdf:writer_pdf_Export";
        }
        return "pdf:writer_pdf_Export:" + String.join(",", options);
    }

    private void cleanupFile(File file) {
        if (file != null && file.exists() && !file.delete()) {
            log.debug("临时文件删除失败: {}", file.getAbsolutePath());
        }
    }

    private void cleanupDirectory(File dir) {
        if (dir == null || !dir.exists()) {
            return;
        }
        File[] children = dir.listFiles();
        if (children != null) {
            for (File child : children) {
                cleanupFile(child);
            }
        }
        if (!dir.delete()) {
            log.debug("临时目录删除失败: {}", dir.getAbsolutePath());
        }
    }

    private void createUnifiedRecord(ConversionRecordDocxToPdf record) {
        ConversionRecord unifiedRecord = new ConversionRecord();
        unifiedRecord.setUserId(record.getUserId());
        unifiedRecord.setIpAddress(record.getIpAddress());
        unifiedRecord.setConversionType(ConversionType.DOCX_TO_PDF.getCode());
        unifiedRecord.setReferenceId(record.getId());
        unifiedRecord.setOriginalFilename(record.getOriginalFilename());
        unifiedRecord.setOutputFilename(record.getPdfFilePath());
        unifiedRecord.setOriginalFileSize(record.getOriginalFileSize());
        unifiedRecord.setOutputFileSize(record.getPdfFileSize());
        unifiedRecord.setConversionStatus(record.getConversionStatus());
        unifiedRecord.setCreateTime(record.getCreateTime());
        unifiedRecord.setUpdateTime(record.getUpdateTime());
        conversionRecordService.createRecord(unifiedRecord);
    }

    private ConversionDocxToPdfResponseDTO buildResponseDTO(ConversionRecordDocxToPdf record) {
        ConversionDocxToPdfResponseDTO dto = new ConversionDocxToPdfResponseDTO();
        BeanUtil.copyProperties(record, dto);

        if (record.getPdfFilePath() != null) {
            String rawUrl = cosFileUtils.getFileUrl(record.getPdfFilePath(), serverDomain + "/api/files/");
            if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
                String encoded = URLEncoder.encode(rawUrl, StandardCharsets.UTF_8);
                String proxyUrl = serverDomain + "/api/files/proxy?url=" + encoded;
                dto.setPdfUrl(proxyUrl);
                dto.setDownloadUrl(proxyUrl + "&download=true");
            } else {
                dto.setPdfUrl(rawUrl);
                dto.setDownloadUrl(rawUrl + (rawUrl.contains("?") ? "&" : "?") + "download=true");
            }
        }

        if (record.getOriginalFileSize() != null) {
            dto.setOriginalFileSizeFormatted(FileUtils.formatFileSize(record.getOriginalFileSize()));
        }
        if (record.getPdfFileSize() != null) {
            dto.setPdfFileSizeFormatted(FileUtils.formatFileSize(record.getPdfFileSize()));
        }
        return dto;
    }
}

